Let's start with a quick overview of the basic syntax, emphasising differences with Python.
Numeric values in Julia work similarly to Python:
Variables in Julia are created as in Python, with a simple assignment operation; variable names can be arbitrary unicode characters. Many may be entered in LaTeX notation, using tab substitution: type \alpha<TAB>
. There is also tab completion on partial names: \alp<TAB>
In [2]:
x = 3
Out[2]:
In [3]:
y = 5
Out[3]:
In [5]:
α = 3; ℵ = 10
Out[5]:
In [ ]:
Functions use parentheses (round brackets, ()
) around the arguments being passed. println
prints its arguments, followed by a new line. [print
omits the new line.]
In [6]:
println("α = ", α)
Simple functions may be defined with a nice mathematical syntax; *
is not needed in simple expressions:
In [12]:
f(x) = 2x^2 + 3x + 1
g(x) = f(x) - (2x+1)*(x+1)
Out[12]:
In [9]:
f(3)
Out[9]:
In [11]:
g(3.5)
The values of variables may be substituted into strings in a simple way using the $
operator:
In [17]:
# Variable substitution with $:
name = "David"
greeting = "Hello, $name"
println(greeting)
More complicated expressions are wrapped in parentheses:
In [13]:
μ = 3
println("The sine of $μ is $(sin(μ))")
There are numerical types with different precisions: typing Float<TAB>
or Int<TAB>
will provide a list. Currently, in arithmetic calculations types are promoted to the machine type. (This looks likely to change soon.)
Machine integers!
In [15]:
a = int(1e16)
a * 10
Out[15]:
In [5]:
a = int8(1)
b = int8(2)
a + b
Out[5]:
In [6]:
typeof(ans)
Out[6]:
These promotion rules are defined in int.jl
.
Arbitrary-precision integers and floating points are available through the types BigInt
and BigFloat
. The function big
converts a number into the corresponding Big
type:
In [16]:
big(10)
Out[16]:
In [17]:
typeof(ans)
Out[17]:
Note that, unlike in Python, integers are not automatically promoted to arbitrary-precision integers.
Exercise: Calculate powers of 10 using standard integers and BigInt
s
In [18]:
10^5
Out[18]:
In [19]:
10**5
In [24]:
10^19
Out[24]:
In [25]:
ten = big(10)
Out[25]:
In [27]:
ten^19
Out[27]:
In [30]:
i = int8(10)
Out[30]:
In [38]:
i * int8(1)
Out[38]:
In [39]:
typeof(ans)
Out[39]:
In [35]:
typemax(Int64)
Out[35]:
In [36]:
typemin(Int64)
Out[36]:
Complex numbers are written using im
for the imaginary part:
In [40]:
a = 7
c = (1+3.5im) * a
Out[40]:
In [41]:
c.im
Out[41]:
In [35]:
c.re, c.im
Out[35]:
In [37]:
c * conj(c) # conj is a function that returns the conjugate of a complex number
Out[37]:
(Tuples behave similarly to Python.)
Rational numbers are also built into Julia; they are created using the //
operator:
In [42]:
3//4
Out[42]:
In [43]:
typeof(ans)
Out[43]:
In [49]:
(big(3)//4)^50
Out[49]:
In [50]:
typeof(ans)
Out[50]:
In [58]:
3//4 + 5//6
Out[58]:
Operators are a convenient way of writing functions:
In [51]:
+(3, 4)
Out[51]:
In [8]:
//(3, 4)
Out[8]:
In [52]:
//
Out[52]:
We see that //
is a function, implemented as a series of methods. We can see what these methods are:
In [53]:
methods(//)
Out[53]:
In [56]:
3 + -34
Out[56]:
The expression n::Integer
is a type annotation that specifies that the method applies when its first argument is of type Integer
.
Clicking on the file name takes us directly to the Julia standard library source code on GitHub where these functions are defined!
To store several values "in one variable", we can try to imitate using a "list" as we would in Python:
In [61]:
l = [3, 4, 5]
Out[61]:
In [59]:
typeof(l)
Out[59]:
In Julia these objects are called Array
s. The curly braces indicate type parameters of the Array
type. The first is the type of element contained in the Array
(all must be of the same type) and the second the number of dimensions.
Exercise: Try to create an array in this way with elements of different types. What happens?
Exercise: What does the following syntax do?: l = {3, 4, 7.5}
In [62]:
l = [3., 4, 5]
Out[62]:
In [64]:
l = [3., "a"]
Out[64]:
In [65]:
l = [3., 'a']
Out[65]:
In [66]:
l = {3., 4, "hello", [3, 4]}
Out[66]:
The indices of Julia arrays are numbered starting at 1, unlike Python (where they are numbered starting at 0).
In [67]:
l[1]
Out[67]:
The syntax for ranges is similar to that for Python:
In [68]:
l[1:2]
Out[68]:
However, the limits must be explicitly specified:
In [69]:
l[2:end] # Use `end` explicitly
Out[69]:
In [70]:
l[1:end-1]
Out[70]:
In [71]:
l[-1]
Julia Arrays
, like Python lists, but unlike numpy
arrays, are dynamic. However, the syntax is rather different from Python -- to add an element at the end of the list, we write
In [72]:
l = [3,4,5]
l + l
Out[72]:
In [74]:
names(l)
Out[74]:
In [75]:
l = [3, 4, 5]
push!(l, 7)
Out[75]:
In [ ]:
sizehint
In [76]:
push!
Out[76]:
In [77]:
methods(push!)
Out[77]:
In [78]:
methodswith(Array)
Out[78]:
In [79]:
l = [3, 4, 5]
push!(l, 12.0)
Out[79]:
In [80]:
12.0 == 12
Out[80]:
In [81]:
push!(l, 12.1)
In [ ]:
append!(l, [10, 11, 12])
push!
replaces append
in Python. There are no methods of objects as in Python; rather, we use functions and send the object as an argument of the function.
The exclamation mark, or bang, (!
) indicates that the function modifies its argument; this is a standard convention in Julia.
Arrays which have been defined with a certain type cannot acquire elements of a different type:
In [10]:
l = [3, 4, 5]
push!(l, "hello")
Array
s work as mathematical vectors, with the sum of two vectors and scalar multiplication being defined:
In [82]:
a = [1.1, 2.2, 3.3]
b = [4.4, 5.5, 6.6]
Out[82]:
In [83]:
a + b
Out[83]:
In [84]:
3.5 * a
Out[84]:
However, operators are, in general, not treated in an elementwise fashion (as they would be e.g. in numpy
):
In [85]:
a * b
Rather, elementwise operations use a Matlab-like syntax, with an extra .
before the symbol for the operator:
In [86]:
a .* b
Out[86]:
There are many useful operations on vectors predefined, without needing to explicitly import them.
In [87]:
dot(a,b) # ans is the last result
Out[87]:
In [88]:
cross(a, b)
Out[88]:
In [89]:
norm(a)
Out[89]:
Use help
or ?
(before the command) to obtain help:
In [ ]:
help(dot)
In [ ]:
?dot
In [91]:
transpose(a)
Out[91]:
In [93]:
a'
Out[93]:
In [94]:
M = [[2,1], [1,1]]
Out[94]:
In [96]:
push!(l, 12.0)
Out[96]:
In [97]:
M = [2 1; 1 1]
Out[97]:
In [99]:
M = reshape([1:8], (2,2,2))
Out[99]:
In [102]:
a ⋅ b
a × b
Out[102]:
I used \cdot
In [103]:
⋅
Out[103]:
[Note that in the Julia command-line REPL, typing ?
puts it immediately into a special help mode. Similarly, ;
puts it into shell mode, in which commands are sent straight to the shell.]
White space in Julia is not significant. Commands on one line can be separated by ;
. Blocks must finish with end
In [105]:
i = 0
while i < 5
print("$i\t")
i += 1
end
In [107]:
total = 0
for i = 1:10
total += i
end
println("Sum is $total")
Here, 1:10
is a range object which may be iterated over.
In [108]:
typeof(1:10)
Out[108]:
We can construct an array from this by enclosing it in square brackets:
In [110]:
collect(1:10)
Out[110]:
In [111]:
[1:2:10, 17]
Out[111]:
Use help
or ?
to get help:
In [113]:
?dot
In [114]:
help(dot)
In [115]:
help("dot")
Exercise: Implement the Babylonian method for calculating the square root of a positive number $y$, via the iteration $$x_{n+1} = \textstyle \frac{1}{2} (x_n + \frac{y}{x_n})$$
In [121]:
♆ = norm
Out[121]:
In [123]:
♆(a)
Out[123]:
In [124]:
snowman(x) = x^2
Out[124]:
In [126]:
☃ = snowman
☃(3)
Out[126]:
In [128]:
a = 3
a < 5 && println("Small") # evaluate the second statement only if the first is true; semantics of if-then
a > 10 || println("Small") # semantics of if not-then
In [129]:
a == 3 ? println("Hello") : println("Not true")
There is an equivalent of list comprehensions in Python, as follows. Note that the array construction syntax is quite flexible.
In [132]:
squares = [i^2 for i in [1:2:10, 7]]
Out[132]:
In [133]:
sums = [i+j for i=1:5, j=1:5]
Out[133]:
In [134]:
sums = [i+j+k for i=1:5, j=1:5, k=1:5]
Out[134]:
Square brackets with commas gives a one-dimensional vector. This is printed in a way that treats it as if it were a column vector (although there is in fact no difference between a one-dimensional row vector and column vector).
In [ ]:
v = [3, 4, 5]
To create explicit matrices, Matlab-style notation is used. If we omit the commas, something different happens: we now obtain a two-dimensional Array
, i.e. a matrix, of size $1 \times n$. [Recall that in the standard notation for matrices, an $m \times n$ matrix has $m$ rows and $n$ columns.]
In [ ]:
row_vec = [3 4 5]
We can also use the transpose operator, '
. [This is actually the conjugate-transpose operator, which also takes the complex conjugate of complex numbers. Transpose without conjugate is denoted .'
]
In [ ]:
row_vec = [1im, 2]'
In [ ]:
row_vec = [1im, 2].'
A complete matrix may be constructed using a semicolon (;
) to separate rows:
In [135]:
M = [1 2; 3 4]
Out[135]:
As in numpy
, it may also be created using a reshape
:
In [ ]:
M = reshape([1, 2, 3, 4], (2,2))
Here, as in Python, (2,2)
denotes an (immutable) tuple:
In [ ]:
t = (2, 2)
typeof(t)
There is an important difference in the way that Python and Julia treat slices of matrices. While in Python a one-dimensional slice in either direction returns a 1-dimensional vector, in Julia there is a difference. A vertical one-dimensional slice gives a 1-dimensional vector (a "column vector"):
In [3]:
M[:,1]
Out[3]:
However, a horizontal one-dimensional slice produces a $1 \times n$ matrix:
In [5]:
M[1,:]
Out[5]:
This is the same result that is produced using the following Matlab-like syntax:
In [6]:
[1 2]
Out[6]:
The Mersenne Twister (pseudo-)random number generator is built-in to Julia:
In [136]:
rand()
Out[136]:
In [137]:
rand(5)
Out[137]:
In [138]:
x = rand(5, 5)
Out[138]:
In [ ]:
rand
In [139]:
v = [1, 2]
Out[139]:
In [140]:
v*v
In [17]:
dot(v, v)
Out[17]:
In [141]:
M = [2 1; 1 1]
Out[141]:
In [142]:
dot(M, v)
Matrix multiplication uses the *
operator:
In [143]:
M * v
Out[143]:
In [144]:
@which M*v
Out[144]:
Exercise: Use the power method to calculate the largest eigenvalue $\lambda_1$ of the matrix $M = \begin{pmatrix} 2 & 1 \\ 1 & 1 \end{pmatrix}$. In this method, we start from an arbitrary non-zero vector $\mathbf{w}$, and repeatedly apply $M$ to it, thus calculating powers of the matrix $M$ applied to $\mathbf{w}$. The resulting vector converges to the eigenvector $\mathbf{v}_1$ corresponding to $\lambda_1$.
In [146]:
w = [1., 1]
M = reshape([2., 1, 1, 1], (2,2))
M, w
Out[146]:
In [148]:
w0 = [1., 1]
w = copy(w0)
for i in 1:10
w_new = M*w
println()
Out[148]:
In [1]:
for i =1:10
t = 3
end
t
Julia has built-in linear algebra, not only using LAPACK, but now also generic routines that work for arbitrary element types, implemented completely in Julia.
For example, given a matrix $A$, the LU-decomposition of $A$ is equivalent to Gaussian elimination; it expresses $A$ as the product $A = LU$, with $L$ a lower-triangular and $U$ an upper-triangular matrix.
This is implemented in pure Julia for arbitrary element types. When the elements are standard floating-point numbers, it uses the corresponding fast LAPACK implementation.
In [2]:
M = rand(100, 100)
eig(M)
Out[2]:
In [3]:
M = rand(100, 100)
M2 = map(big, M)
Out[3]:
In [4]:
lu(M2)
Out[4]:
In [5]:
lu(M)
Out[5]:
In [5]:
methods(lu)
Out[5]:
In [7]:
@edit lu(M)
A Julia script, similar to a Python script, is a sequence of Julia commands placed in a file, with the termination .jl
.
From the command line, a script script.jl
can be run as
julia script.jl arg1 arg2
where arg1
and arg2
are command-line arguments.
These command-line arguments to Julia scripts are placed in the variable ARGS
as an array of strings.
Simple file input and output is easy:
In [66]:
outfile = open("test.txt", "w")
Out[66]:
In [69]:
for i in 1:10
println(outfile, "The value of i is $i")
end
close(outfile)
In [70]:
;cat test.txt
In [72]:
infile = open("test.txt", "r")
Out[72]:
In [74]:
lines = readlines(infile)
Out[74]:
In [75]:
map(split, lines)
Out[75]:
In [76]:
[float(line[6]) for line in map(split, lines)]
Out[76]:
In [49]:
x = rand(5,5)
Out[49]:
In [54]:
writedlm("random.txt", x)
;cat random.txt
In [56]:
y = readdlm("random.txt") # note that tab completion works for files
Out[56]:
In [93]:
;ls
In [9]:
run(`echo Hello`)
Linear algebra, FFT, random numbers, special functions. Packages for optimization, ODEs etc.
Functions may be defined using the short syntax f(x) = 3x + 1
or using a longer form:
In [10]:
dup(x) = 2x
Out[10]:
In [11]:
function duplicate(x)
2x # no explicit "return" needed
end
Out[11]:
The last value computed in the function is automatically returned; no explicit return
statement is required.
In [19]:
duplicate(x) = x^2
Out[19]:
Every operator in Julia is a function. Functions are implemented by specifying their action on different types. Until now, we have written only functions that are generic, in the sense that they do not specify which type they accept, and as in Python they will work as long as the operations performed in them make sense for the input value:
In [13]:
duplicate(3), duplicate(3.5), duplicate(1+3im)
Out[13]:
In [14]:
duplicate("Hola")
In [15]:
2 * "Hola"
Note that string concatenation uses the *
operator in Julia, instead of the +
operator as in Python.
Repeating a string is thus done by raising to an integer power:
In [16]:
"Hello"^2
Out[16]:
As a simple example, suppose that we wish to concatenate two strings. In Python we would write:
In [17]:
s1 = "Hello, "
s2 = "David"
s1 + s2
However, we see that in Julia, summation is not defined for strings. What is it defined for?
In [20]:
+
Out[20]:
We see that +
is treated as a function, and that it has a multitude of methods, which, in Julia, are specialised versions of the function that act on different types:
In [107]:
methods(+)
Out[107]:
If we were unaware of the *
operator for string concatenation, we could just define our own +
for the concatenation of two strings:
In [1]:
+(s1::String, s2::String) = string(s1, s2)
Out[1]:
In [5]:
"First" + " second"
Out[5]:
However, we cannot add a number to a string, since we have not (yet) defined it:
In [2]:
"The value of x is " + 3
This we can also define, using the previous new definition:
In [11]:
+(s::String, x::Number) = s + "$(2x)"
Out[11]:
In [12]:
"The value of x is " + 3
Out[12]:
In [5]:
x = 3.5
"The value of x is " + x
Out[5]:
In [8]:
3 + "hello"
In fact, we can define the summation of a string with any other object:
In [13]:
+(s::String, x) = s + string(x)
Out[13]:
In [14]:
"Complex " + [3,4,5]
Out[14]:
In [15]:
"a" + 3
Out[15]:
In [16]:
Number
Out[16]:
In [17]:
typeof(Number)
Out[17]:
In [18]:
super(Int64)
Out[18]:
In [19]:
super(Signed)
Out[19]:
In [20]:
super(Integer)
Out[20]:
In [21]:
super(Real)
Out[21]:
In [22]:
super(Number)
Out[22]:
In this way, the concept of "function" is replaced by a "patchwork" of different definitions for objects of different types, easily modifiable by the user. This is also exactly the way to define "operator overloading" for user-defined types.
In the above, we also begin to see the power of multiple dispatch: we defined two methods (versions) of the function +
, both with the same number but different types of arguments.
A user-defined "composite type" is a collection of data. Unlike in Python, types do not "own" methods (functions internal to the type).
Rather, methods are defined separately, and are characterised by the types of all of their arguments; this is known as multiple dispatch. (Dispatch is the process of choosing which "version" of a given function to execute.)
A simple, but useful, example, is that of defining a 2D vector type. (See also the ImmutableArrays.jl
package; fixed-size arrays will later be incorporated into base Julia.)
In [26]:
@which 3//4
Out[26]:
In [28]:
Rational(3)
Out[28]:
In [27]:
6//4
Out[27]:
In [25]:
im*im
Out[25]:
In [40]:
immutable Vector2D # type
x::Float64
y::Float64
end
In [32]:
v = Vector2D(3, 4)
w = Vector2D(5, 6)
Out[32]:
In [33]:
v + w
In [34]:
+(v::Vector2D, w::Vector2D) = Vector2D(v.x+w.x, v.y+w.y)
Out[34]:
In [35]:
v + w
Out[35]:
In [36]:
*(v::Vector2D, α::Number) = Vector2D(v.x*α, v.y*α)
*(α::Number, v::Vector2D) = Vector2D(v.x*α, v.y*α)
Out[36]:
In [37]:
v * 3.5
Out[37]:
In [38]:
3.5 * v
Exercise:
Define mathematical operations on Vector2D
. Define a particle with position and velocity in 2D. Define function move
that acts on a particle to move it over a time $\delta t$.
Here, we have used immutable
instead of type
for efficiency: the object is stored in an efficient packed form.
The equivalent of the Python __repr__
method for an object is to extend the show
method:
In [41]:
show
Out[41]:
In [5]:
import Base.show
show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")
Out[5]:
In [44]:
v
Out[44]:
In [4]:
+(v1::Vector2D, v2::Vector2D) = Vector2D(v1.x+v2.x, v1.y+v2.y)
*(v::Vector2D, lamb::Number) = Vector2D(lamb*v.x, lamb*v.y)
Out[4]:
We can confirm that the new method for the function +
has indeed been defined:
In [5]:
methods(+)
Out[5]:
In [7]:
x = Vector2D(3, 4)
y = Vector2D(5, 6)
x + y
Out[7]:
Types may have a parameter, for example:
In [1]:
immutable Vector2D{T <: Real}
x::T
y::T
end
T
is a type parameter.
The expression T <: Real
means that T
must be a subtype of the abstract type Real
.
We can investigate the type hierarchy with the super
function:
In [4]:
Integer
Out[4]:
In [5]:
super(Integer)
Out[5]:
In [6]:
super(Real)
Out[6]:
In [7]:
super(Number)
Out[7]:
In [2]:
v = Vector2D(3., 4.)
Out[2]:
In [3]:
v = Vector2D(3, 4.)
In [4]:
v = Vector2D(3//4, 5//6)
Out[4]:
In [6]:
show{T}(io::IO, v::Vector2D{T}) = print(io, "[$(v.x), $(v.y)]")
Out[6]:
In [7]:
v
Out[7]:
Here, the types of the two arguments were different, so there is no match for the type signature.
We can define outer constructors, defined outside the type definition itself, which allow other ways of constructing the object:
In [8]:
Vector2D{T}(x::T) = Vector2D(x, x)
Out[8]:
In [9]:
Vector2D(3)
Out[9]:
Let's define a particle:
In [8]:
type Particle
position::Vector2D{Float64}
velocity::Vector2D{Float64}
end
move(p::Particle, dt::Real) = p.position += p.velocity * dt
Out[8]:
In [9]:
show(io::IO, p::Particle) = print(io, "pos: $(p.position); vel: $(p.velocity)")
Out[9]:
In [11]:
p = Particle(Vector2D(0.,0.), Vector2D(1.,1.))
Out[11]:
In [13]:
+{T}(v1::Vector2D{T}, v2::Vector2D{T}) = Vector2D{T}(v1.x+v2.x, v1.y+v2.y)
*{T}(v::Vector2D{T}, lamb::Number) = Vector2D{T}(lamb*v.x, lamb*v.y)
Out[13]:
In [14]:
move(p, 0.1)
Out[14]:
In [8]:
p
Out[8]:
Now we can define a gas as a collection of particle:
In [15]:
Int
Out[15]:
In [16]:
type Gas
particles::Vector{Particle} # Array{Particle, 1}
function Gas(N::Integer)
parts = [Particle(Vector2D(rand(2)...), Vector2D(rand(2)...)) for i in 1:N]
new(parts)
end
end
In [17]:
show(io::IO, g::Gas) = for i in 1:length(g.particles); \
println(io, "Particle $i: $(g.particles[i])"); end
Out[17]:
In [18]:
g = Gas(10)
Out[18]:
In [41]:
move(g, 1)
g
Out[41]:
In [36]:
function move(g::Gas, dt::Number)
for particle in g.particles
move(particle, dt)
end
end
Out[36]:
In [99]:
move(g, 1)
In [100]:
g
Out[100]:
In [23]:
M = [1 1; 0 1]
Out[23]:
In [24]:
M * M
Out[24]:
In [25]:
M^10
Out[25]:
In [27]:
M^(-1)
Out[27]:
In [29]:
inv(M)
Out[29]:
In [30]:
det(M)
Out[30]:
In [31]:
norm(M)
Out[31]:
In [32]:
?norm
In [34]:
lamb, vv = eig(M)
Out[34]:
In [23]:
methods(Symmetric)
Out[23]:
Exercise: Generate a random matrix of $1000 \times 1000$ gaussian random variates. Calculate its eigenvalues and plot them. Calculate the differences between consecutive eigenvalues and plot a histogram of them.
In [15]:
N = 1000
M = randn(N, N)
M = Symmetric(M);
In [16]:
typeof(M)
Out[16]:
In [20]:
@which dot([3], [4])
Out[20]:
In [19]:
help("dot")
In [17]:
@time lamb, vv = eig(M);
In [13]:
using PyPlot
In [18]:
plot(lamb, "o-")
Out[18]:
In [50]:
PyPlot.figure(figsize=(8,4))
plot(lamb, "o-")
Out[50]:
In [22]:
differences = diff(lamb);
In [23]:
plot(differences, "o-")
Out[23]:
In [53]:
h = hist(d, 100)
Out[53]:
In [59]:
plot(collect(h[1][1:end-1]), h[2])
Out[59]:
requires vs include vs using vs import
In [70]:
g.particles
Out[70]:
In [71]:
for particle in g.particles
move(particle, 1)
end
g.particles
Out[71]: